<template>
  <view class="container">
    <canvas
      :canvas-id="canvasId"
      :class="['canvas', debug ? 'debug' : 'pro']"
      :style="{ width: pxWidth, height: pxHeight }"
    >
    </canvas>
  </view>
</template>

<script>
export default {
  name: 'WxaCanvas',
  data() {
    return {
      pxWidth: '',
      pxHeight: '',
      canvasId: 'canvasId-WxaCanvas'
    }
  },
  created() {
    const sysInfo = uni.getSystemInfoSync()
    const screenWidth = sysInfo.screenWidth
    this.factor = screenWidth / 750
  },
  methods: {
    /**
     * 渲染块
     * @param {Object} params
     */
    drawBlock({
      text,
      width = 0,
      height,
      x,
      y,
      paddingLeft = 0,
      paddingRight = 0,
      borderWidth,
      backgroundColor,
      borderColor,
      borderRadius = 0,
      opacity = 1
    }) {
      // 判断是否块内有文字
      let blockWidth = 0 // 块的宽度
      let textX = 0
      let textY = 0
      if (typeof text !== 'undefined') {
        // 如果有文字并且块的宽度小于文字宽度，块的宽度为 文字的宽度 + 内边距
        const textWidth = this._getTextWidth(typeof text.text === 'string' ? text : text.text)
        blockWidth = textWidth > width ? textWidth : width
        blockWidth += paddingLeft + paddingLeft

        const { textAlign = 'left', text: textCon } = text
        textY = height / 2 + y // 文字的y轴坐标在块中线
        if (textAlign === 'left') {
          // 如果是右对齐，那x轴在块的最左边
          textX = x + paddingLeft
        } else if (textAlign === 'center') {
          textX = blockWidth / 2 + x
        } else {
          textX = x + blockWidth - paddingRight
        }
      } else {
        blockWidth = width
      }

      if (backgroundColor) {
        // 画面
        this.ctx.save()
        this.ctx.setGlobalAlpha(opacity)
        this.ctx.setFillStyle(backgroundColor)
        if (borderRadius > 0) {
          // 画圆角矩形
          this._drawRadiusRect(x, y, blockWidth, height, borderRadius)
          this.ctx.fill()
        } else {
          this.ctx.fillRect(this.toPx(x), this.toPx(y), this.toPx(blockWidth), this.toPx(height))
        }
        this.ctx.restore()
      }
      if (borderWidth) {
        // 画线
        this.ctx.save()
        this.ctx.setGlobalAlpha(opacity)
        this.ctx.setStrokeStyle(borderColor)
        this.ctx.setLineWidth(this.toPx(borderWidth))
        if (borderRadius > 0) {
          // 画圆角矩形边框
          this._drawRadiusRect(x, y, blockWidth, height, borderRadius)
          this.ctx.stroke()
        } else {
          this.ctx.strokeRect(this.toPx(x), this.toPx(y), this.toPx(blockWidth), this.toPx(height))
        }
        this.ctx.restore()
      }

      if (text) {
        this.drawText(Object.assign(text, { x: textX, y: textY }))
      }
    },

    /**
     * 渲染文字
     * @param {Object} params
     */
    drawText(params) {
      const { x, y, fontSize, color, baseLine, textAlign, text, opacity = 1, width, lineNum, lineHeight } = params
      if (Object.prototype.toString.call(text) === '[object Array]') {
        let preText = { x, y, baseLine }
        text.forEach((item) => {
          preText.x += item.marginLeft || 0
          const textWidth = this._drawSingleText(
            Object.assign(item, {
              ...preText
            })
          )
          preText.x += textWidth + (item.marginRight || 0) // 下一段字的x轴为上一段字x + 上一段字宽度
        })
      } else {
        this._drawSingleText(params)
      }
    },

    /**
     * 渲染图片
     */
    drawImage(data) {
      const { imgPath, x, y, w, h, sx, sy, sw, sh, borderRadius = 0, borderWidth = 0, borderColor } = data
      this.ctx.save()
      if (borderRadius > 0) {
        this._drawRadiusRect(x, y, w, h, borderRadius)
        this.ctx.strokeStyle = 'rgba(255,255,255,0)'
        this.ctx.stroke()
        this.ctx.clip()
        this.ctx.drawImage(
          imgPath,
          this.toPx(sx),
          this.toPx(sy),
          this.toPx(sw),
          this.toPx(sh),
          this.toPx(x),
          this.toPx(y),
          this.toPx(w),
          this.toPx(h)
        )
        if (borderWidth > 0) {
          this.ctx.setStrokeStyle(borderColor)
          this.ctx.setLineWidth(this.toPx(borderWidth))
          this.ctx.stroke()
        }
      } else {
        this.ctx.drawImage(
          imgPath,
          this.toPx(sx),
          this.toPx(sy),
          this.toPx(sw),
          this.toPx(sh),
          this.toPx(x),
          this.toPx(y),
          this.toPx(w),
          this.toPx(h)
        )
      }
      this.ctx.restore()
    },
    /**
     * 渲染线
     * @param {*} param0
     */
    drawLine({ startX, startY, endX, endY, color, width }) {
      this.ctx.save()
      this.ctx.beginPath()
      this.ctx.setStrokeStyle(color)
      this.ctx.setLineWidth(this.toPx(width))
      this.ctx.moveTo(this.toPx(startX), this.toPx(startY))
      this.ctx.lineTo(this.toPx(endX), this.toPx(endY))
      this.ctx.stroke()
      this.ctx.closePath()
      this.ctx.restore()
    },
    downloadResource({ images = [], pixelRatio = 1 }) {
      const drawList = []
      this.drawArr = []
      images.forEach((image, index) => drawList.push(this._downloadImageAndInfo(image, index, pixelRatio)))
      return Promise.all(drawList)
    },
    initCanvas(w, h, debug) {
      this.pxWidth = this.toPx(w) + 'px'
      this.pxHeight = this.toPx(h) + 'px'
      this.debug = debug
      return this.$nextTick()
    },

    /**
     * 画圆角矩形
     */
    _drawRadiusRect(x, y, w, h, r) {
      const br = r / 2
      this.ctx.beginPath()
      this.ctx.moveTo(this.toPx(x + br), this.toPx(y)) // 移动到左上角的点
      this.ctx.lineTo(this.toPx(x + w - br), this.toPx(y))
      this.ctx.arc(
        this.toPx(x + w - br),
        this.toPx(y + br),
        this.toPx(br),
        2 * Math.PI * (3 / 4),
        2 * Math.PI * (4 / 4)
      )
      this.ctx.lineTo(this.toPx(x + w), this.toPx(y + h - br))
      this.ctx.arc(this.toPx(x + w - br), this.toPx(y + h - br), this.toPx(br), 0, 2 * Math.PI * (1 / 4))
      this.ctx.lineTo(this.toPx(x + br), this.toPx(y + h))
      this.ctx.arc(
        this.toPx(x + br),
        this.toPx(y + h - br),
        this.toPx(br),
        2 * Math.PI * (1 / 4),
        2 * Math.PI * (2 / 4)
      )
      this.ctx.lineTo(this.toPx(x), this.toPx(y + br))
      this.ctx.arc(this.toPx(x + br), this.toPx(y + br), this.toPx(br), 2 * Math.PI * (2 / 4), 2 * Math.PI * (3 / 4))
    },
    /**
     * 计算文本长度
     * @param {Array|Object}} text 数组 或者 对象
     */
    _getTextWidth(text) {
      let texts = []
      if (Object.prototype.toString.call(text) === '[object Object]') {
        texts.push(text)
      } else {
        texts = text
      }
      let width = 0
      texts.forEach(({ fontSize, text, marginLeft = 0, marginRight = 0 }) => {
        this.ctx.setFontSize(this.toPx(fontSize))
        width += this.ctx.measureText(text).width + marginLeft + marginRight
      })

      return this.toRpx(width)
    },
    /**
     * 渲染一段文字
     */
    _drawSingleText({
      x,
      y,
      fontSize,
      color,
      baseLine,
      textAlign = 'left',
      text,
      opacity = 1,
      textDecoration = 'none',
      width,
      lineNum = 1,
      lineHeight = 0,
      fontWeight = 'normal',
      fontStyle = 'normal',
      fontFamily = 'sans-serif'
    }) {
      this.ctx.save()
      this.ctx.beginPath()
      this.ctx.font = fontStyle + ' ' + fontWeight + ' ' + this.toPx(fontSize, true) + 'px ' + fontFamily
      this.ctx.setGlobalAlpha(opacity)
      // this.ctx.setFontSize(this.toPx(fontSize));
      this.ctx.setFillStyle(color)
      this.ctx.setTextBaseline(baseLine)
      this.ctx.setTextAlign(textAlign)
      let textWidth = this.toRpx(this.ctx.measureText(text).width)
      const textArr = []
      if (textWidth > width) {
        // 文本宽度 大于 渲染宽度
        let fillText = ''
        let line = 1
        for (let i = 0; i <= text.length - 1; i++) {
          // 将文字转为数组，一行文字一个元素
          fillText = fillText + text[i]
          if (this.toRpx(this.ctx.measureText(fillText).width) >= width) {
            if (line === lineNum) {
              if (i !== text.length - 1) {
                fillText = fillText.substring(0, fillText.length - 1) + '...'
              }
            }
            if (line <= lineNum) {
              textArr.push(fillText)
            }
            fillText = ''
            line++
          } else {
            if (line <= lineNum) {
              if (i === text.length - 1) {
                textArr.push(fillText)
              }
            }
          }
        }
        textWidth = width
      } else {
        textArr.push(text)
      }

      textArr.forEach((item, index) => {
        this.ctx.fillText(item, this.toPx(x), this.toPx(y + (lineHeight || fontSize) * index))
      })

      this.ctx.restore()

      // textDecoration
      if (textDecoration !== 'none') {
        let lineY = y
        if (textDecoration === 'line-through') {
          // 目前只支持贯穿线
          lineY = y

          // 小程序画布baseLine偏移阈值
          let threshold = 5

          // 根据baseLine的不同对贯穿线的Y坐标做相应调整
          switch (baseLine) {
            case 'top':
              lineY += fontSize / 2 + threshold
              break
            case 'middle':
              break
            case 'bottom':
              lineY -= fontSize / 2 + threshold
              break
            default:
              lineY -= fontSize / 2 - threshold
              break
          }
        }
        this.ctx.save()
        this.ctx.moveTo(this.toPx(x), this.toPx(lineY))
        this.ctx.lineTo(this.toPx(x) + this.toPx(textWidth), this.toPx(lineY))
        this.ctx.setStrokeStyle(color)
        this.ctx.stroke()
        this.ctx.restore()
      }

      return textWidth
    },

    /**
     * 下载图片并获取图片信息
     */
    _downloadImageAndInfo(image, index, pixelRatio) {
      return new Promise((resolve, reject) => {
        const { x, y, url, zIndex } = image
        const imageUrl = url
        // 下载图片
        this._downImage(imageUrl, index)
          // 获取图片信息
          .then((imgPath) => this._getImageInfo(imgPath, index))
          .then(({ imgPath, imgInfo }) => {
            // 根据画布的宽高计算出图片绘制的大小，这里会保证图片绘制不变形
            let sx
            let sy
            const borderRadius = image.borderRadius || 0
            const setWidth = image.width
            const setHeight = image.height
            const width = this.toRpx(imgInfo.width / pixelRatio)
            const height = this.toRpx(imgInfo.height / pixelRatio)

            if (width / height <= setWidth / setHeight) {
              sx = 0
              sy = (height - (width / setWidth) * setHeight) / 2
            } else {
              sy = 0
              sx = (width - (height / setHeight) * setWidth) / 2
            }
            this.drawArr.push({
              type: 'image',
              borderRadius,
              borderWidth: image.borderWidth,
              borderColor: image.borderColor,
              zIndex: typeof zIndex !== 'undefined' ? zIndex : index,
              imgPath,
              sx,
              sy,
              sw: width - sx * 2,
              sh: height - sy * 2,
              x,
              y,
              w: setWidth,
              h: setHeight
            })
            resolve()
          })
          .catch((err) => reject(err))
      })
    },
    /**
     * 下载图片资源
     * @param {*} imageUrl
     */
    _downImage(imageUrl) {
      return new Promise((resolve, reject) => {
        if (/^http/.test(imageUrl)) {
          uni.downloadFile({
            url: this._mapHttpToHttps(imageUrl),
            success: (res) => {
              if (res.statusCode === 200) {
                resolve(res.tempFilePath)
              } else {
                reject(res.errMsg)
              }
            },
            fail(err) {
              reject(err)
            }
          })
        } else {
          // 支持本地地址
          resolve(imageUrl)
        }
      })
    },
    /**
     * 获取图片信息
     * @param {*} imgPath
     * @param {*} index
     */
    _getImageInfo(imgPath, index) {
      return new Promise((resolve, reject) => {
        uni.getImageInfo({
          src: imgPath,
          success(res) {
            resolve({ imgPath, imgInfo: res, index })
          },
          fail(err) {
            reject(err)
          }
        })
      })
    },
    toPx(rpx, int) {
      if (int) {
        return parseInt(rpx * this.factor * this.pixelRatio)
      }
      return rpx * this.factor * this.pixelRatio
    },
    toRpx(px, int) {
      if (int) {
        return parseInt(px / this.factor)
      }
      return px / this.factor
    },
    /**
     * 将http转为https
     * @param {String}} rawUrl 图片资源url
     */
    _mapHttpToHttps(rawUrl) {
      if (rawUrl.indexOf(':') < 0) {
        return rawUrl
      }
      const urlComponent = rawUrl.split(':')
      if (urlComponent.length === 2) {
        if (urlComponent[0] === 'http') {
          urlComponent[0] = 'https'
          return `${urlComponent[0]}:${urlComponent[1]}`
        }
      }
      return rawUrl
    },

    /**
     * 计算画布的高度
     * @param {*} config
     */
    getHeight(config) {
      const getTextHeight = (text) => {
        let fontHeight = text.lineHeight || text.fontSize
        let height = 0
        if (text.baseLine === 'top') {
          height = fontHeight
        } else if (text.baseLine === 'middle') {
          height = fontHeight / 2
        } else {
          height = 0
        }
        return height
      }
      const heightArr = []
      ;(config.blocks || []).forEach((item) => {
        heightArr.push(item.y + item.height)
      })
      ;(config.texts || []).forEach((item) => {
        let height
        if (Object.prototype.toString.call(item.text) === '[object Array]') {
          item.text.forEach((i) => {
            height = getTextHeight({ ...i, baseLine: item.baseLine })
            heightArr.push(item.y + height)
          })
        } else {
          height = getTextHeight(item)
          heightArr.push(item.y + height)
        }
      })
      ;(config.images || []).forEach((item) => {
        heightArr.push(item.y + item.height)
      })
      ;(config.lines || []).forEach((item) => {
        heightArr.push(item.startY)
        heightArr.push(item.endY)
      })
      const sortRes = heightArr.sort((a, b) => b - a)
      let canvasHeight = 0
      if (sortRes.length > 0) {
        canvasHeight = sortRes[0]
      }
      if (config.height < canvasHeight || !config.height) {
        return canvasHeight
      } else {
        return config.height
      }
    },
    create(config) {
      this.ctx = uni.createCanvasContext(this.canvasId, this)

      this.pixelRatio = config.pixelRatio || 1
      const height = this.getHeight(config)
      this.initCanvas(config.width, height, config.debug)
        .then(() => {
          // 设置画布底色
          if (config.backgroundColor) {
            this.ctx.save()
            this.ctx.setFillStyle(config.backgroundColor)
            this.ctx.fillRect(0, 0, this.toPx(config.width), this.toPx(height))
            this.ctx.restore()
          }
          const { texts = [], images = [], blocks = [], lines = [] } = config
          const queue = this.drawArr
            .concat(
              texts.map((item) => {
                item.type = 'text'
                item.zIndex = item.zIndex || 0
                return item
              })
            )
            .concat(
              blocks.map((item) => {
                item.type = 'block'
                item.zIndex = item.zIndex || 0
                return item
              })
            )
            .concat(
              lines.map((item) => {
                item.type = 'line'
                item.zIndex = item.zIndex || 0
                return item
              })
            )
          // 按照顺序排序
          queue.sort((a, b) => a.zIndex - b.zIndex)

          queue.forEach((item) => {
            if (item.type === 'image') {
              this.drawImage(item)
            } else if (item.type === 'text') {
              this.drawText(item)
            } else if (item.type === 'block') {
              this.drawBlock(item)
            } else if (item.type === 'line') {
              this.drawLine(item)
            }
          })

          const res = uni.getSystemInfoSync()
          const platform = res.platform
          let time = 0
          if (platform === 'android') {
            // 在安卓平台，经测试发现如果海报过于复杂在转换时需要做延时，要不然样式会错乱
            time = 300
          }
          this.ctx.draw(false, () => {
            setTimeout(() => {
              uni.canvasToTempFilePath(
                {
                  canvasId: this.canvasId,
                  success: (res) => {
                    this.$emit('success', res.tempFilePath)
                  },
                  fail: (err) => {
                    this.$emit('fail', err)
                  }
                },
                this
              )
            }, time)
          })
        })
        .catch((err) => {
          uni.showToast({ icon: 'none', title: err.errMsg || '生成失败' })
          console.error(err)
        })
    }
  }
}
</script>

<style lang="scss" scoped>
.canvas {
  width: 750rpx;
  height: 750rpx;
}
.canvas.pro {
  position: absolute;
  bottom: 0;
  left: 0;
  transform: translate3d(-9999rpx, 0, 0);
}
.canvas.debug {
  position: absolute;
  bottom: 0;
  left: 0;
  border: 1rpx solid #ccc;
}
</style>
